import { setTimeout } from 'timers/promises';
import { Assets } from '../Assets';
import { basePath, getApp } from '@test-utils';
import { Texture } from '~/rendering';
import { Spritesheet } from '~/spritesheet';

import type { BundleIdentifierOptions } from '../resolver/Resolver';

describe('Assets bundles', () =>
{
    beforeAll(async () =>
    {
        // ensure app is loaded before running tests
        await getApp();
    });

    beforeEach(() =>
    {
        // reset the loader
        Assets.reset();
    });

    it('should add and load bundle', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', [
            { alias: 'bunny', src: 'textures/bunny.{png,webp}' },
            { alias: 'spritesheet', src: 'spritesheet/spritesheet.json' },
        ]);

        const assets = await Assets.loadBundle('testBundle');

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should give a correct progress update', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', [
            { alias: ['bunny', 'b'], src: 'textures/bunny.{png,webp}' },
            { alias: 'spritesheet', src: 'spritesheet/spritesheet.json' },
        ]);

        let progress = 0;
        const progressMock = jest.fn((p) => { progress = p; });
        const assets = await Assets.loadBundle('testBundle', progressMock);

        expect(progressMock).toHaveBeenCalledTimes(2);
        expect(progress).toBe(1);

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should give a correct progress update with progress size', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', [
            { alias: ['bunny', 'b'], src: 'textures/bunny.{png,webp}', progressSize: 100 },
            { alias: 'spritesheet', src: 'spritesheet/spritesheet.json', progressSize: 900 },
        ]);

        let progress = 0;
        let progressFirst = 0;
        const progressMock = jest.fn((p) =>
        {
            progress = p;
            if (progressFirst === 0) progressFirst = p;
        });
        const assets = await Assets.loadBundle('testBundle', progressMock);

        expect(progressMock).toHaveBeenCalledTimes(2);
        expect(progress).toBe(1);
        expect([0.1, 0.9]).toContain(progressFirst);

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should give a correct progress update with multiple bundles and progress size', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', [
            {
                alias: ['bunny', 'b'], src: [{
                    src: 'textures/bunny.png',
                    progressSize: 400 // should not be used as webp is supported
                }, {
                    src: 'textures/bunny.webp',
                    progressSize: 100
                }]
            },
            { alias: 'spritesheet', src: 'spritesheet/spritesheet.json', progressSize: 900 },
        ]);
        Assets.addBundle('testBundle2', [
            { alias: ['bunny2', 'b2'], src: 'textures/bunny.{png,webp}', progressSize: 200 },
            { alias: 'spritesheet2', src: 'spritesheet/spritesheet.json', progressSize: 800 },
        ]);

        let progress = 0;
        const progressPercentages: number[] = [];
        const progressMock = jest.fn((p) =>
        {
            progress = p;
            progressPercentages.push(p - (progressPercentages.reduce((a, b) => a + b, 0)));
        });
        const assets = await Assets.loadBundle(['testBundle', 'testBundle2'], progressMock);

        expect(progressMock).toHaveBeenCalledTimes(4);
        expect(progress).toBe(1);
        const expectedValues = [0.05, 0.45, 0.1, 0.4];

        expect(progressPercentages).toHaveLength(4);
        expectedValues.forEach((expectedValue) =>
        {
            expect(progressPercentages.some((actual) =>
                Math.abs(actual - expectedValue) < 0.001
            )).toBe(true);
        });

        expect(assets.testBundle.bunny).toBeInstanceOf(Texture);
        expect(assets.testBundle.spritesheet).toBeInstanceOf(Spritesheet);
        expect(assets.testBundle2.bunny2).toBeInstanceOf(Texture);
        expect(assets.testBundle2.spritesheet2).toBeInstanceOf(Spritesheet);
    });

    it('should add and load bundle object', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', {
            bunny: 'textures/bunny.{png,webp}',
            spritesheet: 'spritesheet/spritesheet.json',
        });

        const assets = await Assets.loadBundle('testBundle');

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should load a bundle found in the manifest', async () =>
    {
        await Assets.init({
            basePath,
            manifest: 'json/asset-manifest-2.json',
        });

        const assets = await Assets.loadBundle('default');

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets['profile-abel']).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should load an object bundle found in the manifest', async () =>
    {
        await Assets.init({
            basePath,
            manifest: 'json/asset-manifest-object.json',
        });

        const assets = await Assets.loadBundle('default');

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets['profile-abel']).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);
    });

    it('should load multiple bundles', async () =>
    {
        await Assets.init({
            basePath,
            manifest: 'json/asset-manifest-2.json',
        });

        const progressMock = jest.fn();

        const assets = await Assets.loadBundle(['default', 'data'], progressMock);

        expect(progressMock).toHaveBeenCalledTimes(4);
        expect(progressMock.mock.calls).toEqual([[0.25], [0.5], [0.75], [1]]);
        expect(assets.default.bunny).toBeInstanceOf(Texture);
        expect(assets.default['profile-abel']).toBeInstanceOf(Texture);
        expect(assets.default.spritesheet).toBeInstanceOf(Spritesheet);

        expect(assets.data[`test.json`]).toEqual({ testNumber: 23, testString: 'Test String 23' });
    });

    it('should background load bundles', async () =>
    {
        await Assets.init({
            basePath,
            manifest: 'json/asset-manifest-2.json',
        });

        await Assets.backgroundLoadBundle('default');

        await setTimeout(500);

        const expectTypes = {
            'json/asset-manifest-2.json': Object,
            'textures/bunny.png': Texture,
            'textures/profile-abel@2x.webp': Texture,
            'spritesheet/spritesheet.json': Spritesheet,
            'spritesheet/spritesheet.png': Texture,
        };

        for (const [key, type] of Object.entries(expectTypes))
        {
            const asset = await Assets.loader.promiseCache[basePath + key].promise;

            expect(asset).toBeInstanceOf(type);
        }
    });

    it('should unload bundles correctly', async () =>
    {
        await Assets.init({
            basePath,
        });

        Assets.addBundle('testBundle', [
            { alias: 'bunny', src: 'textures/bunny.{png,webp}' },
            { alias: 'spritesheet', src: 'spritesheet/spritesheet.json' },
        ]);

        const assets = await Assets.loadBundle('testBundle');

        expect(assets.bunny).toBeInstanceOf(Texture);
        expect(assets.spritesheet).toBeInstanceOf(Spritesheet);

        await Assets.unloadBundle('testBundle');

        expect(assets.bunny.baseTexture).toBe(null);
    });

    it('should load bundles with clashing names correctly', async () =>
    {
        const manifest = {
            bundles: [
                {
                    name: 'bunny1',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny.png',
                        },
                    ],
                },
                {
                    name: 'bunny2',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny-2.png',
                        },
                    ],
                },
            ]
        };

        await Assets.init({ manifest, basePath });

        const resources = await Assets.loadBundle('bunny1');
        const resources2 = await Assets.loadBundle('bunny2');

        expect(resources.character).not.toBe(resources2.character);
    });

    it('should load bundles with clashing names correctly with custom connector', async () =>
    {
        const manifest = {
            bundles: [
                {
                    name: 'bunny1',
                    assets: [
                        {
                            alias: ['character', 'character2'],
                            src: 'textures/bunny.png',
                        },
                    ],
                },
                {
                    name: 'bunny2',
                    assets: [
                        {
                            alias: ['character', 'character2'],
                            src: 'textures/bunny-2.png',
                        },
                    ],
                },
            ]
        };

        const bundleIdentifier: BundleIdentifierOptions = {
            connector: ':',
        };

        await Assets.init({ manifest, basePath, bundleIdentifier });

        const resources = await Assets.loadBundle('bunny1');
        const resources2 = await Assets.loadBundle('bunny2');

        expect(resources.character).not.toBe(resources2.character);

        const bunny2Character = await Assets.load('bunny2:character');

        expect(resources.character).not.toBe(bunny2Character.character);
        expect(resources2.character).not.toBe(bunny2Character.character);
    });

    it('should load bundles with clashing names correctly and overridden bundleIdentifier', async () =>
    {
        const manifest = {
            bundles: [
                {
                    name: 'bunny1',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny.png',
                        },
                    ],
                },
                {
                    name: 'bunny2',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny-2.png',
                        },
                    ],
                },
            ]
        };

        const bundleIdentifier: BundleIdentifierOptions = {
            createBundleAssetId: (bundleId, assetId) => `${assetId}>-<${bundleId}`,
            extractAssetIdFromBundle: (bundleId, assetId) => assetId.replace(`>-<${bundleId}`, ''),
        };

        await Assets.init({ manifest, basePath, bundleIdentifier });

        const resources = await Assets.loadBundle('bunny1');
        const resources2 = await Assets.loadBundle('bunny2');

        expect(resources.character).not.toBe(resources2.character);

        const bunny2Character = await Assets.load('character>-<bunny2');

        expect(resources.character).not.toBe(bunny2Character.character);
        expect(resources2.character).not.toBe(bunny2Character.character);
    });

    it('should throw an error if bundleIdentifier is overridden but does not pair up correctly', async () =>
    {
        const manifest = {
            bundles: [
                {
                    name: 'bunny1',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny.png',
                        },
                    ],
                },
                {
                    name: 'bunny2',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny-2.png',
                        },
                    ],
                },
            ]
        };

        const bundleIdentifier: BundleIdentifierOptions = {
            createBundleAssetId: (bundleId, assetId) => `${assetId}>-<${bundleId}`,
            extractAssetIdFromBundle: (_bundleId, _assetId) => 'no-idea',
        };

        // expect promise to throw an error..
        await expect(Assets.init({ manifest, basePath, bundleIdentifier })).rejects.toThrow();
    });

    it('should pass optional data correctly in a bundle', async () =>
    {
        const manifest = {
            bundles: [
                {
                    name: 'bunny1',
                    assets: [
                        {
                            alias: 'character',
                            src: 'textures/bunny.png',
                            data: {
                                otherData: 'thing'
                            }
                        },
                    ],

                },
            ]
        };

        await Assets.init({ manifest, basePath });

        // expect promise to throw an error..
        // eslint-disable-next-line jest/valid-expect
        expect(async () => await Assets.init({ manifest, basePath }));

        const bundle = Assets.resolver.resolveBundle('bunny1');

        expect(bundle.character.data).toEqual({ otherData: 'thing' });
    });

    it('should use the throw loading strategy by default', async () =>
    {
        let count = 0;

        await Assets.init({
            basePath,
            loadOptions: { onError: () => count++ },
        });

        Assets.addBundle('testBundle', [
            { alias: 'bunny', src: 'textures/bunny_no_img.png' },
            { alias: 'bunny2', src: 'textures/bunny.png' },
        ]);

        await Assets.loadBundle('testBundle').catch(
            (e) => expect(e).toBeInstanceOf(Error)
        );

        expect(Assets.loader.loadOptions.strategy).toBe('throw');
        expect(count).toBe(1);
    });

    it('should use the retry loading strategy when set', async () =>
    {
        let count = 0;

        await Assets.init({
            basePath,
            loadOptions: { strategy: 'retry', onError: () => count++ },
        });

        Assets.addBundle('testBundle', [
            { alias: 'bunny', src: 'textures/bunny_no_img.png' },
            { alias: 'bunny2', src: 'textures/bunny.png' },
        ]);

        await Assets.loadBundle('testBundle').catch(
            (e) => expect(e).toBeInstanceOf(Error)
        );

        expect(count).toBe(4); // 1 initial try + 3 retries
    }, 5000);

    it('should use the skip loading strategy when set', async () =>
    {
        let count = 0;

        await Assets.init({
            basePath,
            loadOptions: { strategy: 'skip', onError: () => count++ },
        });

        Assets.addBundle('testBundle', [
            { alias: 'bunny', src: 'textures/bunny_no_img.png' },
            { alias: 'bunny2', src: 'textures/bunny.png' },
        ]);

        const assets = await Assets.loadBundle('testBundle');

        expect(assets.bunny).toBeUndefined();
        expect(assets.bunny2).toBeDefined();
        expect(count).toBe(1);
    });
});
